﻿using System;
using System.Runtime.Serialization;

namespace Ns
{
    /// <summary>
    /// Represents a <see cref="Recurrence"/> executed 
    /// every <see cref="Weeks"/>(s)
    /// on specified <see cref="WeekDays"/>
    /// at a specified <see cref="TimeOfDay"/>.
    /// This class cannot be inherited.
    /// </summary>
    [Serializable]
    [DataContract]
    public sealed class WeeklyRecurrence : Recurrence
    {
        #region Fields

        DateTime?[] dueDates = new DateTime?[7];
        
        #endregion

        #region Properties

        /// <summary>
        /// Gets the time of day.
        /// </summary>
        [DataMember(EmitDefaultValue = false)]
        public TimeSpan TimeOfDay { get; internal set; }

        /// <summary>
        /// Gets the recurrent week(s) (ex: every 2 weeks).
        /// </summary>
        [DataMember(EmitDefaultValue = false)]
        public int Weeks { get; internal set; }
        
        /// <summary>
        /// Gets the recurrent week days (ex: every <see cref="RecurrentWeekDays.WeekEnd"/>).
        /// </summary>
        [DataMember(EmitDefaultValue = false)]
        public RecurrentWeekDays WeekDays { get; internal set; }

        #endregion

        #region Methods

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return string.Format("Occurs at {0} every {1} week(s) on {2}"
                , TimeOfDay
                , Weeks
                , WeekDays);
        }

        /// <summary>
        /// Clones this instance.
        /// </summary>
        /// <returns>A copy of this instance.</returns>
        public override object Clone()
        {
            return new WeeklyRecurrence(TimeOfDay, WeekDays, Weeks);
        }

        /// <summary>
        /// Gets the date and time of the next occurrence from <paramref name="now"/>.
        /// </summary>
        /// <param name="now">The current date and time assumed to be an universal time if not specified to avoid ambiguous dates.</param>
        /// <returns>The date and time of the next occurrence in local time.</returns>
        protected override DateTime GetNextDateTime(DateTime now)
        {
            //assume universal time
            now = now.AsUniversalTime();

            //correct inexistent dates (ex: 27/03/2011 02:30:00 > 27/03/2011 03:30:30)
            now = now.ToUniversalTime().ToLocalTime();

            //consider today to maintain the specified timeOfDay across the daylight saving time period
            var today = (now.TimeOfDay < TimeOfDay)
                ? now.Date.AddDays(-1)
                : now.Date;

            var nextDate = GetNextDateByDayOfWeek(today);
            
            var index = (int)nextDate.DayOfWeek;
            if (dueDates[index] != null && 
                dueDates[index].Value < nextDate)
                nextDate = dueDates[index].Value.AddDays(7 * Weeks);
            dueDates[index] = nextDate;

            // add the specified timeOfDay
            return nextDate.Add(TimeOfDay);
        }

        private DateTime GetNextDateByDayOfWeek(DateTime value)
        {
            var match = default(RecurrentWeekDays);

            do
            {
                value = value.AddDays(1);
                match = FlagsEnum.PowerOfTwo<RecurrentWeekDays>(value.DayOfWeek);
            }
            while ((WeekDays & match) != match);

            return value;
        }

        #endregion

        #region Constructors

        private WeeklyRecurrence()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="WeeklyRecurrence"/> class
        /// executed weekly on the specified <paramref name="weekDays"/> 
        /// at the specified <paramref name="timeOfDay"/>.
        /// </summary>
        /// <param name="timeOfDay">The time of day the recurrence is executed.</param>
        /// <param name="weekDays">The recurrent week days (ex: every <see cref="RecurrentWeekDays.WeekEnd"/>).</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="timeOfDay"/> is lower than 00:00:00 or greater than or equal to 1.00:00:00.
        /// </exception>
        public WeeklyRecurrence(TimeSpan timeOfDay, RecurrentWeekDays weekDays)
            : this(timeOfDay, weekDays, 1)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="WeeklyRecurrence"/> class
        /// executed every <paramref name="weeks"/>(s)
        /// on the specified <paramref name="weekDays"/> 
        /// at the specified <paramref name="timeOfDay"/>.
        /// </summary>
        /// <param name="timeOfDay">The time of day the recurrence is executed.</param>
        /// <param name="weekDays">The recurrent week days (ex: every <see cref="RecurrentWeekDays.WeekEnd"/>).</param>
        /// <param name="weeks">The recurrent week(s) (ex: every 2 weeks).</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="timeOfDay"/> is lower than 00:00:00 or greater than or equal to 1.00:00:00 -or-
        /// <paramref name="weeks"/> is not strictly positive.
        /// </exception>
        public WeeklyRecurrence(TimeSpan timeOfDay, RecurrentWeekDays weekDays, int weeks)
        {
            if (timeOfDay < TimeSpan.Zero || timeOfDay >= TimeSpan.FromDays(1))
                throw new ArgumentOutOfRangeException("timeOfDay");

            if (weeks <= 0)
                throw new ArgumentOutOfRangeException("weeks");
            
            TimeOfDay = timeOfDay;
            Weeks = weeks;
            WeekDays = weekDays;
        }

        #endregion
    }
}